/*
 * Copyright (c) 2000, 2015, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package com.sun.imageio.plugins.jpeg;

import javax.imageio.IIOException;
import javax.imageio.ImageWriter;
import javax.imageio.ImageWriteParam;
import javax.imageio.IIOImage;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
import javax.imageio.plugins.jpeg.JPEGQTable;
import javax.imageio.plugins.jpeg.JPEGHuffmanTable;

import org.w3c.dom.Node;

import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.awt.image.DataBufferByte;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.ColorConvertOp;
import java.awt.image.RenderedImage;
import java.awt.image.BufferedImage;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.Transparency;

import java.io.IOException;

import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;

import sun.java2d.Disposer;
import sun.java2d.DisposerRecord;

public class JPEGImageWriter extends ImageWriter {

  ///////// Private variables

  private boolean debug = false;

  /**
   * The following variable contains a pointer to the IJG library
   * structure for this reader.  It is assigned in the constructor
   * and then is passed in to every native call.  It is set to 0
   * by dispose to avoid disposing twice.
   */
  private long structPointer = 0;


  /**
   * The output stream we write to
   */
  private ImageOutputStream ios = null;

  /**
   * The Raster we will write from
   */
  private Raster srcRas = null;

  /**
   * An intermediate Raster holding compressor-friendly data
   */
  private WritableRaster raster = null;

  /**
   * Set to true if we are writing an image with an
   * indexed ColorModel
   */
  private boolean indexed = false;
  private IndexColorModel indexCM = null;

  private boolean convertTosRGB = false;  // Used by PhotoYCC only
  private WritableRaster converted = null;

  private boolean isAlphaPremultiplied = false;
  private ColorModel srcCM = null;

  /**
   * If there are thumbnails to be written, this is the list.
   */
  private List thumbnails = null;

  /**
   * If metadata should include an icc profile, store it here.
   */
  private ICC_Profile iccProfile = null;

  private int sourceXOffset = 0;
  private int sourceYOffset = 0;
  private int sourceWidth = 0;
  private int[] srcBands = null;
  private int sourceHeight = 0;

  /**
   * Used when calling listeners
   */
  private int currentImage = 0;

  private ColorConvertOp convertOp = null;

  private JPEGQTable[] streamQTables = null;
  private JPEGHuffmanTable[] streamDCHuffmanTables = null;
  private JPEGHuffmanTable[] streamACHuffmanTables = null;

  // Parameters for writing metadata
  private boolean ignoreJFIF = false;  // If it's there, use it
  private boolean forceJFIF = false;  // Add one for the thumbnails
  private boolean ignoreAdobe = false;  // If it's there, use it
  private int newAdobeTransform = JPEG.ADOBE_IMPOSSIBLE;  // Change if needed
  private boolean writeDefaultJFIF = false;
  private boolean writeAdobe = false;
  private JPEGMetadata metadata = null;

  private boolean sequencePrepared = false;

  private int numScans = 0;

  /**
   * The referent to be registered with the Disposer.
   */
  private Object disposerReferent = new Object();

  /**
   * The DisposerRecord that handles the actual disposal of this writer.
   */
  private DisposerRecord disposerRecord;

  ///////// End of Private variables

  ///////// Protected variables

  protected static final int WARNING_DEST_IGNORED = 0;
  protected static final int WARNING_STREAM_METADATA_IGNORED = 1;
  protected static final int WARNING_DEST_METADATA_COMP_MISMATCH = 2;
  protected static final int WARNING_DEST_METADATA_JFIF_MISMATCH = 3;
  protected static final int WARNING_DEST_METADATA_ADOBE_MISMATCH = 4;
  protected static final int WARNING_IMAGE_METADATA_JFIF_MISMATCH = 5;
  protected static final int WARNING_IMAGE_METADATA_ADOBE_MISMATCH = 6;
  protected static final int WARNING_METADATA_NOT_JPEG_FOR_RASTER = 7;
  protected static final int WARNING_NO_BANDS_ON_INDEXED = 8;
  protected static final int WARNING_ILLEGAL_THUMBNAIL = 9;
  protected static final int WARNING_IGNORING_THUMBS = 10;
  protected static final int WARNING_FORCING_JFIF = 11;
  protected static final int WARNING_THUMB_CLIPPED = 12;
  protected static final int WARNING_METADATA_ADJUSTED_FOR_THUMB = 13;
  protected static final int WARNING_NO_RGB_THUMB_AS_INDEXED = 14;
  protected static final int WARNING_NO_GRAY_THUMB_AS_INDEXED = 15;

  private static final int MAX_WARNING = WARNING_NO_GRAY_THUMB_AS_INDEXED;

  ///////// End of Protected variables

  ///////// static initializer

  static {
    java.security.AccessController.doPrivileged(
        new java.security.PrivilegedAction<Void>() {
          public Void run() {
            System.loadLibrary("jpeg");
            return null;
          }
        });
    initWriterIDs(JPEGQTable.class,
        JPEGHuffmanTable.class);
  }

  //////// Public API

  public JPEGImageWriter(ImageWriterSpi originator) {
    super(originator);
    structPointer = initJPEGImageWriter();
    disposerRecord = new JPEGWriterDisposerRecord(structPointer);
    Disposer.addRecord(disposerReferent, disposerRecord);
  }

  public void setOutput(Object output) {
    setThreadLock();
    try {
      cbLock.check();

      super.setOutput(output); // validates output
      resetInternalState();
      ios = (ImageOutputStream) output; // so this will always work
      // Set the native destination
      setDest(structPointer);
    } finally {
      clearThreadLock();
    }
  }

  public ImageWriteParam getDefaultWriteParam() {
    return new JPEGImageWriteParam(null);
  }

  public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) {
    setThreadLock();
    try {
      return new JPEGMetadata(param, this);
    } finally {
      clearThreadLock();
    }
  }

  public IIOMetadata
  getDefaultImageMetadata(ImageTypeSpecifier imageType,
      ImageWriteParam param) {
    setThreadLock();
    try {
      return new JPEGMetadata(imageType, param, this);
    } finally {
      clearThreadLock();
    }
  }

  public IIOMetadata convertStreamMetadata(IIOMetadata inData,
      ImageWriteParam param) {
    // There isn't much we can do.  If it's one of ours, then
    // return it.  Otherwise just return null.  We use it only
    // for tables, so we can't get a default and modify it,
    // as this will usually not be what is intended.
    if (inData instanceof JPEGMetadata) {
      JPEGMetadata jpegData = (JPEGMetadata) inData;
      if (jpegData.isStream) {
        return inData;
      }
    }
    return null;
  }

  public IIOMetadata
  convertImageMetadata(IIOMetadata inData,
      ImageTypeSpecifier imageType,
      ImageWriteParam param) {
    setThreadLock();
    try {
      return convertImageMetadataOnThread(inData, imageType, param);
    } finally {
      clearThreadLock();
    }
  }

  private IIOMetadata
  convertImageMetadataOnThread(IIOMetadata inData,
      ImageTypeSpecifier imageType,
      ImageWriteParam param) {
    // If it's one of ours, just return it
    if (inData instanceof JPEGMetadata) {
      JPEGMetadata jpegData = (JPEGMetadata) inData;
      if (!jpegData.isStream) {
        return inData;
      } else {
        // Can't convert stream metadata to image metadata
        // XXX Maybe this should put out a warning?
        return null;
      }
    }
    // If it's not one of ours, create a default and set it from
    // the standard tree from the input, if it exists.
    if (inData.isStandardMetadataFormatSupported()) {
      String formatName =
          IIOMetadataFormatImpl.standardMetadataFormatName;
      Node tree = inData.getAsTree(formatName);
      if (tree != null) {
        JPEGMetadata jpegData = new JPEGMetadata(imageType,
            param,
            this);
        try {
          jpegData.setFromTree(formatName, tree);
        } catch (IIOInvalidTreeException e) {
          // Other plug-in generates bogus standard tree
          // XXX Maybe this should put out a warning?
          return null;
        }

        return jpegData;
      }
    }
    return null;
  }

  public int getNumThumbnailsSupported(ImageTypeSpecifier imageType,
      ImageWriteParam param,
      IIOMetadata streamMetadata,
      IIOMetadata imageMetadata) {
    if (jfifOK(imageType, param, streamMetadata, imageMetadata)) {
      return Integer.MAX_VALUE;
    }
    return 0;
  }

  static final Dimension[] preferredThumbSizes = {new Dimension(1, 1),
      new Dimension(255, 255)};

  public Dimension[] getPreferredThumbnailSizes(ImageTypeSpecifier imageType,
      ImageWriteParam param,
      IIOMetadata streamMetadata,
      IIOMetadata imageMetadata) {
    if (jfifOK(imageType, param, streamMetadata, imageMetadata)) {
      return (Dimension[]) preferredThumbSizes.clone();
    }
    return null;
  }

  private boolean jfifOK(ImageTypeSpecifier imageType,
      ImageWriteParam param,
      IIOMetadata streamMetadata,
      IIOMetadata imageMetadata) {
    // If the image type and metadata are JFIF compatible, return true
    if ((imageType != null) &&
        (!JPEG.isJFIFcompliant(imageType, true))) {
      return false;
    }
    if (imageMetadata != null) {
      JPEGMetadata metadata = null;
      if (imageMetadata instanceof JPEGMetadata) {
        metadata = (JPEGMetadata) imageMetadata;
      } else {
        metadata = (JPEGMetadata) convertImageMetadata(imageMetadata,
            imageType,
            param);
      }
      // metadata must have a jfif node
      if (metadata.findMarkerSegment
          (JFIFMarkerSegment.class, true) == null) {
        return false;
      }
    }
    return true;
  }

  public boolean canWriteRasters() {
    return true;
  }

  public void write(IIOMetadata streamMetadata,
      IIOImage image,
      ImageWriteParam param) throws IOException {
    setThreadLock();
    try {
      cbLock.check();

      writeOnThread(streamMetadata, image, param);
    } finally {
      clearThreadLock();
    }
  }

  private void writeOnThread(IIOMetadata streamMetadata,
      IIOImage image,
      ImageWriteParam param) throws IOException {

    if (ios == null) {
      throw new IllegalStateException("Output has not been set!");
    }

    if (image == null) {
      throw new IllegalArgumentException("image is null!");
    }

    // if streamMetadata is not null, issue a warning
    if (streamMetadata != null) {
      warningOccurred(WARNING_STREAM_METADATA_IGNORED);
    }

    // Obtain the raster and image, if there is one
    boolean rasterOnly = image.hasRaster();

    RenderedImage rimage = null;
    if (rasterOnly) {
      srcRas = image.getRaster();
    } else {
      rimage = image.getRenderedImage();
      if (rimage instanceof BufferedImage) {
        // Use the Raster directly.
        srcRas = ((BufferedImage) rimage).getRaster();
      } else if (rimage.getNumXTiles() == 1 &&
          rimage.getNumYTiles() == 1) {
        // Get the unique tile.
        srcRas = rimage.getTile(rimage.getMinTileX(),
            rimage.getMinTileY());

        // Ensure the Raster has dimensions of the image,
        // as the tile dimensions might differ.
        if (srcRas.getWidth() != rimage.getWidth() ||
            srcRas.getHeight() != rimage.getHeight()) {
          srcRas = srcRas.createChild(srcRas.getMinX(),
              srcRas.getMinY(),
              rimage.getWidth(),
              rimage.getHeight(),
              srcRas.getMinX(),
              srcRas.getMinY(),
              null);
        }
      } else {
        // Image is tiled so get a contiguous raster by copying.
        srcRas = rimage.getData();
      }
    }

    // Now determine if we are using a band subset

    // By default, we are using all source bands
    int numSrcBands = srcRas.getNumBands();
    indexed = false;
    indexCM = null;
    ColorModel cm = null;
    ColorSpace cs = null;
    isAlphaPremultiplied = false;
    srcCM = null;
    if (!rasterOnly) {
      cm = rimage.getColorModel();
      if (cm != null) {
        cs = cm.getColorSpace();
        if (cm instanceof IndexColorModel) {
          indexed = true;
          indexCM = (IndexColorModel) cm;
          numSrcBands = cm.getNumComponents();
        }
        if (cm.isAlphaPremultiplied()) {
          isAlphaPremultiplied = true;
          srcCM = cm;
        }
      }
    }

    srcBands = JPEG.bandOffsets[numSrcBands - 1];
    int numBandsUsed = numSrcBands;
    // Consult the param to determine if we're writing a subset

    if (param != null) {
      int[] sBands = param.getSourceBands();
      if (sBands != null) {
        if (indexed) {
          warningOccurred(WARNING_NO_BANDS_ON_INDEXED);
        } else {
          srcBands = sBands;
          numBandsUsed = srcBands.length;
          if (numBandsUsed > numSrcBands) {
            throw new IIOException
                ("ImageWriteParam specifies too many source bands");
          }
        }
      }
    }

    boolean usingBandSubset = (numBandsUsed != numSrcBands);
    boolean fullImage = ((!rasterOnly) && (!usingBandSubset));

    int[] bandSizes = null;
    if (!indexed) {
      bandSizes = srcRas.getSampleModel().getSampleSize();
      // If this is a subset, we must adjust bandSizes
      if (usingBandSubset) {
        int[] temp = new int[numBandsUsed];
        for (int i = 0; i < numBandsUsed; i++) {
          temp[i] = bandSizes[srcBands[i]];
        }
        bandSizes = temp;
      }
    } else {
      int[] tempSize = srcRas.getSampleModel().getSampleSize();
      bandSizes = new int[numSrcBands];
      for (int i = 0; i < numSrcBands; i++) {
        bandSizes[i] = tempSize[0];  // All the same
      }
    }

    for (int i = 0; i < bandSizes.length; i++) {
      // 4450894 part 1: The IJG libraries are compiled so they only
      // handle <= 8-bit samples.  We now check the band sizes and throw
      // an exception for images, such as USHORT_GRAY, with > 8 bits
      // per sample.
      if (bandSizes[i] <= 0 || bandSizes[i] > 8) {
        throw new IIOException("Illegal band size: should be 0 < size <= 8");
      }
      // 4450894 part 2: We expand IndexColorModel images to full 24-
      // or 32-bit in grabPixels() for each scanline.  For indexed
      // images such as BYTE_BINARY, we need to ensure that we update
      // bandSizes to account for the scaling from 1-bit band sizes
      // to 8-bit.
      if (indexed) {
        bandSizes[i] = 8;
      }
    }

    if (debug) {
      System.out.println("numSrcBands is " + numSrcBands);
      System.out.println("numBandsUsed is " + numBandsUsed);
      System.out.println("usingBandSubset is " + usingBandSubset);
      System.out.println("fullImage is " + fullImage);
      System.out.print("Band sizes:");
      for (int i = 0; i < bandSizes.length; i++) {
        System.out.print(" " + bandSizes[i]);
      }
      System.out.println();
    }

    // Destination type, if there is one
    ImageTypeSpecifier destType = null;
    if (param != null) {
      destType = param.getDestinationType();
      // Ignore dest type if we are writing a complete image
      if ((fullImage) && (destType != null)) {
        warningOccurred(WARNING_DEST_IGNORED);
        destType = null;
      }
    }

    // Examine the param

    sourceXOffset = srcRas.getMinX();
    sourceYOffset = srcRas.getMinY();
    int imageWidth = srcRas.getWidth();
    int imageHeight = srcRas.getHeight();
    sourceWidth = imageWidth;
    sourceHeight = imageHeight;
    int periodX = 1;
    int periodY = 1;
    int gridX = 0;
    int gridY = 0;
    JPEGQTable[] qTables = null;
    JPEGHuffmanTable[] DCHuffmanTables = null;
    JPEGHuffmanTable[] ACHuffmanTables = null;
    boolean optimizeHuffman = false;
    JPEGImageWriteParam jparam = null;
    int progressiveMode = ImageWriteParam.MODE_DISABLED;

    if (param != null) {

      Rectangle sourceRegion = param.getSourceRegion();
      if (sourceRegion != null) {
        Rectangle imageBounds = new Rectangle(sourceXOffset,
            sourceYOffset,
            sourceWidth,
            sourceHeight);
        sourceRegion = sourceRegion.intersection(imageBounds);
        sourceXOffset = sourceRegion.x;
        sourceYOffset = sourceRegion.y;
        sourceWidth = sourceRegion.width;
        sourceHeight = sourceRegion.height;
      }

      if (sourceWidth + sourceXOffset > imageWidth) {
        sourceWidth = imageWidth - sourceXOffset;
      }
      if (sourceHeight + sourceYOffset > imageHeight) {
        sourceHeight = imageHeight - sourceYOffset;
      }

      periodX = param.getSourceXSubsampling();
      periodY = param.getSourceYSubsampling();
      gridX = param.getSubsamplingXOffset();
      gridY = param.getSubsamplingYOffset();

      switch (param.getCompressionMode()) {
        case ImageWriteParam.MODE_DISABLED:
          throw new IIOException("JPEG compression cannot be disabled");
        case ImageWriteParam.MODE_EXPLICIT:
          float quality = param.getCompressionQuality();
          quality = JPEG.convertToLinearQuality(quality);
          qTables = new JPEGQTable[2];
          qTables[0] = JPEGQTable.K1Luminance.getScaledInstance
              (quality, true);
          qTables[1] = JPEGQTable.K2Chrominance.getScaledInstance
              (quality, true);
          break;
        case ImageWriteParam.MODE_DEFAULT:
          qTables = new JPEGQTable[2];
          qTables[0] = JPEGQTable.K1Div2Luminance;
          qTables[1] = JPEGQTable.K2Div2Chrominance;
          break;
        // We'll handle the metadata case later
      }

      progressiveMode = param.getProgressiveMode();

      if (param instanceof JPEGImageWriteParam) {
        jparam = (JPEGImageWriteParam) param;
        optimizeHuffman = jparam.getOptimizeHuffmanTables();
      }
    }

    // Now examine the metadata
    IIOMetadata mdata = image.getMetadata();
    if (mdata != null) {
      if (mdata instanceof JPEGMetadata) {
        metadata = (JPEGMetadata) mdata;
        if (debug) {
          System.out.println
              ("We have metadata, and it's JPEG metadata");
        }
      } else {
        if (!rasterOnly) {
          ImageTypeSpecifier type = destType;
          if (type == null) {
            type = new ImageTypeSpecifier(rimage);
          }
          metadata = (JPEGMetadata) convertImageMetadata(mdata,
              type,
              param);
        } else {
          warningOccurred(WARNING_METADATA_NOT_JPEG_FOR_RASTER);
        }
      }
    }

    // First set a default state

    ignoreJFIF = false;  // If it's there, use it
    ignoreAdobe = false;  // If it's there, use it
    newAdobeTransform = JPEG.ADOBE_IMPOSSIBLE;  // Change if needed
    writeDefaultJFIF = false;
    writeAdobe = false;

    // By default we'll do no conversion:
    int inCsType = JPEG.JCS_UNKNOWN;
    int outCsType = JPEG.JCS_UNKNOWN;

    JFIFMarkerSegment jfif = null;
    AdobeMarkerSegment adobe = null;
    SOFMarkerSegment sof = null;

    if (metadata != null) {
      jfif = (JFIFMarkerSegment) metadata.findMarkerSegment
          (JFIFMarkerSegment.class, true);
      adobe = (AdobeMarkerSegment) metadata.findMarkerSegment
          (AdobeMarkerSegment.class, true);
      sof = (SOFMarkerSegment) metadata.findMarkerSegment
          (SOFMarkerSegment.class, true);
    }

    iccProfile = null;  // By default don't write one
    convertTosRGB = false;  // PhotoYCC does this
    converted = null;

    if (destType != null) {
      if (numBandsUsed != destType.getNumBands()) {
        throw new IIOException
            ("Number of source bands != number of destination bands");
      }
      cs = destType.getColorModel().getColorSpace();
      // Check the metadata against the destination type
      if (metadata != null) {
        checkSOFBands(sof, numBandsUsed);

        checkJFIF(jfif, destType, false);
        // Do we want to write an ICC profile?
        if ((jfif != null) && (ignoreJFIF == false)) {
          if (JPEG.isNonStandardICC(cs)) {
            iccProfile = ((ICC_ColorSpace) cs).getProfile();
          }
        }
        checkAdobe(adobe, destType, false);

      } else { // no metadata, but there is a dest type
        // If we can add a JFIF or an Adobe marker segment, do so
        if (JPEG.isJFIFcompliant(destType, false)) {
          writeDefaultJFIF = true;
          // Do we want to write an ICC profile?
          if (JPEG.isNonStandardICC(cs)) {
            iccProfile = ((ICC_ColorSpace) cs).getProfile();
          }
        } else {
          int transform = JPEG.transformForType(destType, false);
          if (transform != JPEG.ADOBE_IMPOSSIBLE) {
            writeAdobe = true;
            newAdobeTransform = transform;
          }
        }
        // re-create the metadata
        metadata = new JPEGMetadata(destType, null, this);
      }
      inCsType = getSrcCSType(destType);
      outCsType = getDefaultDestCSType(destType);
    } else { // no destination type
      if (metadata == null) {
        if (fullImage) {  // no dest, no metadata, full image
          // Use default metadata matching the image and param
          metadata = new JPEGMetadata(new ImageTypeSpecifier(rimage),
              param, this);
          if (metadata.findMarkerSegment
              (JFIFMarkerSegment.class, true) != null) {
            cs = rimage.getColorModel().getColorSpace();
            if (JPEG.isNonStandardICC(cs)) {
              iccProfile = ((ICC_ColorSpace) cs).getProfile();
            }
          }

          inCsType = getSrcCSType(rimage);
          outCsType = getDefaultDestCSType(rimage);
        }
        // else no dest, no metadata, not an image,
        // so no special headers, no color conversion
      } else { // no dest type, but there is metadata
        checkSOFBands(sof, numBandsUsed);
        if (fullImage) {  // no dest, metadata, image
          // Check that the metadata and the image match

          ImageTypeSpecifier inputType =
              new ImageTypeSpecifier(rimage);

          inCsType = getSrcCSType(rimage);

          if (cm != null) {
            boolean alpha = cm.hasAlpha();
            switch (cs.getType()) {
              case ColorSpace.TYPE_GRAY:
                if (!alpha) {
                  outCsType = JPEG.JCS_GRAYSCALE;
                } else {
                  if (jfif != null) {
                    ignoreJFIF = true;
                    warningOccurred
                        (WARNING_IMAGE_METADATA_JFIF_MISMATCH);
                  }
                  // out colorspace remains unknown
                }
                if ((adobe != null)
                    && (adobe.transform != JPEG.ADOBE_UNKNOWN)) {
                  newAdobeTransform = JPEG.ADOBE_UNKNOWN;
                  warningOccurred
                      (WARNING_IMAGE_METADATA_ADOBE_MISMATCH);
                }
                break;
              case ColorSpace.TYPE_RGB:
                if (!alpha) {
                  if (jfif != null) {
                    outCsType = JPEG.JCS_YCbCr;
                    if (JPEG.isNonStandardICC(cs)
                        || ((cs instanceof ICC_ColorSpace)
                        && (jfif.iccSegment != null))) {
                      iccProfile =
                          ((ICC_ColorSpace) cs).getProfile();
                    }
                  } else if (adobe != null) {
                    switch (adobe.transform) {
                      case JPEG.ADOBE_UNKNOWN:
                        outCsType = JPEG.JCS_RGB;
                        break;
                      case JPEG.ADOBE_YCC:
                        outCsType = JPEG.JCS_YCbCr;
                        break;
                      default:
                        warningOccurred
                            (WARNING_IMAGE_METADATA_ADOBE_MISMATCH);
                        newAdobeTransform = JPEG.ADOBE_UNKNOWN;
                        outCsType = JPEG.JCS_RGB;
                        break;
                    }
                  } else {
                    // consult the ids
                    int outCS = sof.getIDencodedCSType();
                    // if they don't resolve it,
                    // consult the sampling factors
                    if (outCS != JPEG.JCS_UNKNOWN) {
                      outCsType = outCS;
                    } else {
                      boolean subsampled =
                          isSubsampled(sof.componentSpecs);
                      if (subsampled) {
                        outCsType = JPEG.JCS_YCbCr;
                      } else {
                        outCsType = JPEG.JCS_RGB;
                      }
                    }
                  }
                } else { // RGBA
                  if (jfif != null) {
                    ignoreJFIF = true;
                    warningOccurred
                        (WARNING_IMAGE_METADATA_JFIF_MISMATCH);
                  }
                  if (adobe != null) {
                    if (adobe.transform
                        != JPEG.ADOBE_UNKNOWN) {
                      newAdobeTransform = JPEG.ADOBE_UNKNOWN;
                      warningOccurred
                          (WARNING_IMAGE_METADATA_ADOBE_MISMATCH);
                    }
                    outCsType = JPEG.JCS_RGBA;
                  } else {
                    // consult the ids
                    int outCS = sof.getIDencodedCSType();
                    // if they don't resolve it,
                    // consult the sampling factors
                    if (outCS != JPEG.JCS_UNKNOWN) {
                      outCsType = outCS;
                    } else {
                      boolean subsampled =
                          isSubsampled(sof.componentSpecs);
                      outCsType = subsampled ?
                          JPEG.JCS_YCbCrA : JPEG.JCS_RGBA;
                    }
                  }
                }
                break;
              case ColorSpace.TYPE_3CLR:
                if (cs == JPEG.JCS.getYCC()) {
                  if (!alpha) {
                    if (jfif != null) {
                      convertTosRGB = true;
                      convertOp =
                          new ColorConvertOp(cs,
                              JPEG.JCS.sRGB,
                              null);
                      outCsType = JPEG.JCS_YCbCr;
                    } else if (adobe != null) {
                      if (adobe.transform
                          != JPEG.ADOBE_YCC) {
                        newAdobeTransform = JPEG.ADOBE_YCC;
                        warningOccurred
                            (WARNING_IMAGE_METADATA_ADOBE_MISMATCH);
                      }
                      outCsType = JPEG.JCS_YCC;
                    } else {
                      outCsType = JPEG.JCS_YCC;
                    }
                  } else { // PhotoYCCA
                    if (jfif != null) {
                      ignoreJFIF = true;
                      warningOccurred
                          (WARNING_IMAGE_METADATA_JFIF_MISMATCH);
                    } else if (adobe != null) {
                      if (adobe.transform
                          != JPEG.ADOBE_UNKNOWN) {
                        newAdobeTransform
                            = JPEG.ADOBE_UNKNOWN;
                        warningOccurred
                            (WARNING_IMAGE_METADATA_ADOBE_MISMATCH);
                      }
                    }
                    outCsType = JPEG.JCS_YCCA;
                  }
                }
            }
          }
        } // else no dest, metadata, not an image.  Defaults ok
      }
    }

    boolean metadataProgressive = false;
    int[] scans = null;

    if (metadata != null) {
      if (sof == null) {
        sof = (SOFMarkerSegment) metadata.findMarkerSegment
            (SOFMarkerSegment.class, true);
      }
      if ((sof != null) && (sof.tag == JPEG.SOF2)) {
        metadataProgressive = true;
        if (progressiveMode == ImageWriteParam.MODE_COPY_FROM_METADATA) {
          scans = collectScans(metadata, sof);  // Might still be null
        } else {
          numScans = 0;
        }
      }
      if (jfif == null) {
        jfif = (JFIFMarkerSegment) metadata.findMarkerSegment
            (JFIFMarkerSegment.class, true);
      }
    }

    thumbnails = image.getThumbnails();
    int numThumbs = image.getNumThumbnails();
    forceJFIF = false;
    // determine if thumbnails can be written
    // If we are going to add a default JFIF marker segment,
    // then thumbnails can be written
    if (!writeDefaultJFIF) {
      // If there is no metadata, then we can't write thumbnails
      if (metadata == null) {
        thumbnails = null;
        if (numThumbs != 0) {
          warningOccurred(WARNING_IGNORING_THUMBS);
        }
      } else {
        // There is metadata
        // If we are writing a raster or subbands,
        // then the user must specify JFIF on the metadata
        if (fullImage == false) {
          if (jfif == null) {
            thumbnails = null;  // Or we can't include thumbnails
            if (numThumbs != 0) {
              warningOccurred(WARNING_IGNORING_THUMBS);
            }
          }
        } else {  // It is a full image, and there is metadata
          if (jfif == null) {  // Not JFIF
            // Can it have JFIF?
            if ((outCsType == JPEG.JCS_GRAYSCALE)
                || (outCsType == JPEG.JCS_YCbCr)) {
              if (numThumbs != 0) {
                forceJFIF = true;
                warningOccurred(WARNING_FORCING_JFIF);
              }
            } else {  // Nope, not JFIF-compatible
              thumbnails = null;
              if (numThumbs != 0) {
                warningOccurred(WARNING_IGNORING_THUMBS);
              }
            }
          }
        }
      }
    }

    // Set up a boolean to indicate whether we need to call back to
    // write metadata
    boolean haveMetadata =
        ((metadata != null) || writeDefaultJFIF || writeAdobe);

    // Now that we have dealt with metadata, finalize our tables set up

    // Are we going to write tables?  By default, yes.
    boolean writeDQT = true;
    boolean writeDHT = true;

    // But if the metadata has no tables, no.
    DQTMarkerSegment dqt = null;
    DHTMarkerSegment dht = null;

    int restartInterval = 0;

    if (metadata != null) {
      dqt = (DQTMarkerSegment) metadata.findMarkerSegment
          (DQTMarkerSegment.class, true);
      dht = (DHTMarkerSegment) metadata.findMarkerSegment
          (DHTMarkerSegment.class, true);
      DRIMarkerSegment dri =
          (DRIMarkerSegment) metadata.findMarkerSegment
              (DRIMarkerSegment.class, true);
      if (dri != null) {
        restartInterval = dri.restartInterval;
      }

      if (dqt == null) {
        writeDQT = false;
      }
      if (dht == null) {
        writeDHT = false;  // Ignored if optimizeHuffman is true
      }
    }

    // Whether we write tables or not, we need to figure out which ones
    // to use
    if (qTables == null) { // Get them from metadata, or use defaults
      if (dqt != null) {
        qTables = collectQTablesFromMetadata(metadata);
      } else if (streamQTables != null) {
        qTables = streamQTables;
      } else if ((jparam != null) && (jparam.areTablesSet())) {
        qTables = jparam.getQTables();
      } else {
        qTables = JPEG.getDefaultQTables();
      }

    }

    // If we are optimizing, we don't want any tables.
    if (optimizeHuffman == false) {
      // If they were for progressive scans, we can't use them.
      if ((dht != null) && (metadataProgressive == false)) {
        DCHuffmanTables = collectHTablesFromMetadata(metadata, true);
        ACHuffmanTables = collectHTablesFromMetadata(metadata, false);
      } else if (streamDCHuffmanTables != null) {
        DCHuffmanTables = streamDCHuffmanTables;
        ACHuffmanTables = streamACHuffmanTables;
      } else if ((jparam != null) && (jparam.areTablesSet())) {
        DCHuffmanTables = jparam.getDCHuffmanTables();
        ACHuffmanTables = jparam.getACHuffmanTables();
      } else {
        DCHuffmanTables = JPEG.getDefaultHuffmanTables(true);
        ACHuffmanTables = JPEG.getDefaultHuffmanTables(false);
      }
    }

    // By default, ids are 1 - N, no subsampling
    int[] componentIds = new int[numBandsUsed];
    int[] HsamplingFactors = new int[numBandsUsed];
    int[] VsamplingFactors = new int[numBandsUsed];
    int[] QtableSelectors = new int[numBandsUsed];
    for (int i = 0; i < numBandsUsed; i++) {
      componentIds[i] = i + 1; // JFIF compatible
      HsamplingFactors[i] = 1;
      VsamplingFactors[i] = 1;
      QtableSelectors[i] = 0;
    }

    // Now override them with the contents of sof, if there is one,
    if (sof != null) {
      for (int i = 0; i < numBandsUsed; i++) {
        if (forceJFIF == false) {  // else use JFIF-compatible default
          componentIds[i] = sof.componentSpecs[i].componentId;
        }
        HsamplingFactors[i] = sof.componentSpecs[i].HsamplingFactor;
        VsamplingFactors[i] = sof.componentSpecs[i].VsamplingFactor;
        QtableSelectors[i] = sof.componentSpecs[i].QtableSelector;
      }
    }

    sourceXOffset += gridX;
    sourceWidth -= gridX;
    sourceYOffset += gridY;
    sourceHeight -= gridY;

    int destWidth = (sourceWidth + periodX - 1) / periodX;
    int destHeight = (sourceHeight + periodY - 1) / periodY;

    // Create an appropriate 1-line databuffer for writing
    int lineSize = sourceWidth * numBandsUsed;

    DataBufferByte buffer = new DataBufferByte(lineSize);

    // Create a raster from that
    int[] bandOffs = JPEG.bandOffsets[numBandsUsed - 1];

    raster = Raster.createInterleavedRaster(buffer,
        sourceWidth, 1,
        lineSize,
        numBandsUsed,
        bandOffs,
        null);

    // Call the writer, who will call back for every scanline

    clearAbortRequest();
    cbLock.lock();
    try {
      processImageStarted(currentImage);
    } finally {
      cbLock.unlock();
    }

    boolean aborted = false;

    if (debug) {
      System.out.println("inCsType: " + inCsType);
      System.out.println("outCsType: " + outCsType);
    }

    // Note that getData disables acceleration on buffer, but it is
    // just a 1-line intermediate data transfer buffer that does not
    // affect the acceleration of the source image.
    aborted = writeImage(structPointer,
        buffer.getData(),
        inCsType, outCsType,
        numBandsUsed,
        bandSizes,
        sourceWidth,
        destWidth, destHeight,
        periodX, periodY,
        qTables,
        writeDQT,
        DCHuffmanTables,
        ACHuffmanTables,
        writeDHT,
        optimizeHuffman,
        (progressiveMode
            != ImageWriteParam.MODE_DISABLED),
        numScans,
        scans,
        componentIds,
        HsamplingFactors,
        VsamplingFactors,
        QtableSelectors,
        haveMetadata,
        restartInterval);

    cbLock.lock();
    try {
      if (aborted) {
        processWriteAborted();
      } else {
        processImageComplete();
      }

      ios.flush();
    } finally {
      cbLock.unlock();
    }
    currentImage++;  // After a successful write
  }

  @Override
  public boolean canWriteSequence() {
    return true;
  }

  public void prepareWriteSequence(IIOMetadata streamMetadata)
      throws IOException {
    setThreadLock();
    try {
      cbLock.check();

      prepareWriteSequenceOnThread(streamMetadata);
    } finally {
      clearThreadLock();
    }
  }

  private void prepareWriteSequenceOnThread(IIOMetadata streamMetadata)
      throws IOException {
    if (ios == null) {
      throw new IllegalStateException("Output has not been set!");
    }

        /*
         * from jpeg_metadata.html:
         * If no stream metadata is supplied to
         * <code>ImageWriter.prepareWriteSequence</code>, then no
         * tables-only image is written.  If stream metadata containing
         * no tables is supplied to
         * <code>ImageWriter.prepareWriteSequence</code>, then a tables-only
         * image containing default visually lossless tables is written.
         */
    if (streamMetadata != null) {
      if (streamMetadata instanceof JPEGMetadata) {
        // write a complete tables-only image at the beginning of
        // the stream.
        JPEGMetadata jmeta = (JPEGMetadata) streamMetadata;
        if (jmeta.isStream == false) {
          throw new IllegalArgumentException
              ("Invalid stream metadata object.");
        }
        // Check that we are
        // at the beginning of the stream, or can go there, and haven't
        // written out the metadata already.
        if (currentImage != 0) {
          throw new IIOException
              ("JPEG Stream metadata must precede all images");
        }
        if (sequencePrepared == true) {
          throw new IIOException("Stream metadata already written!");
        }

        // Set the tables
        // If the metadata has no tables, use default tables.
        streamQTables = collectQTablesFromMetadata(jmeta);
        if (debug) {
          System.out.println("after collecting from stream metadata, "
              + "streamQTables.length is "
              + streamQTables.length);
        }
        if (streamQTables == null) {
          streamQTables = JPEG.getDefaultQTables();
        }
        streamDCHuffmanTables =
            collectHTablesFromMetadata(jmeta, true);
        if (streamDCHuffmanTables == null) {
          streamDCHuffmanTables = JPEG.getDefaultHuffmanTables(true);
        }
        streamACHuffmanTables =
            collectHTablesFromMetadata(jmeta, false);
        if (streamACHuffmanTables == null) {
          streamACHuffmanTables = JPEG.getDefaultHuffmanTables(false);
        }

        // Now write them out
        writeTables(structPointer,
            streamQTables,
            streamDCHuffmanTables,
            streamACHuffmanTables);
      } else {
        throw new IIOException("Stream metadata must be JPEG metadata");
      }
    }
    sequencePrepared = true;
  }

  public void writeToSequence(IIOImage image, ImageWriteParam param)
      throws IOException {
    setThreadLock();
    try {
      cbLock.check();

      if (sequencePrepared == false) {
        throw new IllegalStateException("sequencePrepared not called!");
      }
      // In the case of JPEG this does nothing different from write
      write(null, image, param);
    } finally {
      clearThreadLock();
    }
  }

  public void endWriteSequence() throws IOException {
    setThreadLock();
    try {
      cbLock.check();

      if (sequencePrepared == false) {
        throw new IllegalStateException("sequencePrepared not called!");
      }
      sequencePrepared = false;
    } finally {
      clearThreadLock();
    }
  }

  public synchronized void abort() {
    setThreadLock();
    try {
      /**
       * NB: we do not check the call back lock here, we allow to abort
       * the reader any time.
       */
      super.abort();
      abortWrite(structPointer);
    } finally {
      clearThreadLock();
    }
  }

  @Override
  protected synchronized void clearAbortRequest() {
    setThreadLock();
    try {
      cbLock.check();
      if (abortRequested()) {
        super.clearAbortRequest();
        // reset C structures
        resetWriter(structPointer);
        // reset the native destination
        setDest(structPointer);
      }
    } finally {
      clearThreadLock();
    }
  }

  private void resetInternalState() {
    // reset C structures
    resetWriter(structPointer);

    // reset local Java structures
    srcRas = null;
    raster = null;
    convertTosRGB = false;
    currentImage = 0;
    numScans = 0;
    metadata = null;
  }

  public void reset() {
    setThreadLock();
    try {
      cbLock.check();

      super.reset();
    } finally {
      clearThreadLock();
    }
  }

  public void dispose() {
    setThreadLock();
    try {
      cbLock.check();

      if (structPointer != 0) {
        disposerRecord.dispose();
        structPointer = 0;
      }
    } finally {
      clearThreadLock();
    }
  }

  ////////// End of public API

  ///////// Package-access API

  /**
   * Called by the native code or other classes to signal a warning.
   * The code is used to lookup a localized message to be used when
   * sending warnings to listeners.
   */
  void warningOccurred(int code) {
    cbLock.lock();
    try {
      if ((code < 0) || (code > MAX_WARNING)) {
        throw new InternalError("Invalid warning index");
      }
      processWarningOccurred
          (currentImage,
              "com.sun.imageio.plugins.jpeg.JPEGImageWriterResources",
              Integer.toString(code));
    } finally {
      cbLock.unlock();
    }
  }

  /**
   * The library has it's own error facility that emits warning messages.
   * This routine is called by the native code when it has already
   * formatted a string for output.
   * XXX  For truly complete localization of all warning messages,
   * the sun_jpeg_output_message routine in the native code should
   * send only the codes and parameters to a method here in Java,
   * which will then format and send the warnings, using localized
   * strings.  This method will have to deal with all the parameters
   * and formats (%u with possibly large numbers, %02d, %02x, etc.)
   * that actually occur in the JPEG library.  For now, this prevents
   * library warnings from being printed to stderr.
   */
  void warningWithMessage(String msg) {
    cbLock.lock();
    try {
      processWarningOccurred(currentImage, msg);
    } finally {
      cbLock.unlock();
    }
  }

  void thumbnailStarted(int thumbnailIndex) {
    cbLock.lock();
    try {
      processThumbnailStarted(currentImage, thumbnailIndex);
    } finally {
      cbLock.unlock();
    }
  }

  // Provide access to protected superclass method
  void thumbnailProgress(float percentageDone) {
    cbLock.lock();
    try {
      processThumbnailProgress(percentageDone);
    } finally {
      cbLock.unlock();
    }
  }

  // Provide access to protected superclass method
  void thumbnailComplete() {
    cbLock.lock();
    try {
      processThumbnailComplete();
    } finally {
      cbLock.unlock();
    }
  }

  ///////// End of Package-access API

  ///////// Private methods

  ///////// Metadata handling

  private void checkSOFBands(SOFMarkerSegment sof, int numBandsUsed)
      throws IIOException {
    // Does the metadata frame header, if any, match numBandsUsed?
    if (sof != null) {
      if (sof.componentSpecs.length != numBandsUsed) {
        throw new IIOException
            ("Metadata components != number of destination bands");
      }
    }
  }

  private void checkJFIF(JFIFMarkerSegment jfif,
      ImageTypeSpecifier type,
      boolean input) {
    if (jfif != null) {
      if (!JPEG.isJFIFcompliant(type, input)) {
        ignoreJFIF = true;  // type overrides metadata
        warningOccurred(input
            ? WARNING_IMAGE_METADATA_JFIF_MISMATCH
            : WARNING_DEST_METADATA_JFIF_MISMATCH);
      }
    }
  }

  private void checkAdobe(AdobeMarkerSegment adobe,
      ImageTypeSpecifier type,
      boolean input) {
    if (adobe != null) {
      int rightTransform = JPEG.transformForType(type, input);
      if (adobe.transform != rightTransform) {
        warningOccurred(input
            ? WARNING_IMAGE_METADATA_ADOBE_MISMATCH
            : WARNING_DEST_METADATA_ADOBE_MISMATCH);
        if (rightTransform == JPEG.ADOBE_IMPOSSIBLE) {
          ignoreAdobe = true;
        } else {
          newAdobeTransform = rightTransform;
        }
      }
    }
  }

  /**
   * Collect all the scan info from the given metadata, and
   * organize it into the scan info array required by the
   * IJG libray.  It is much simpler to parse out this
   * data in Java and then just copy the data in C.
   */
  private int[] collectScans(JPEGMetadata metadata,
      SOFMarkerSegment sof) {
    List segments = new ArrayList();
    int SCAN_SIZE = 9;
    int MAX_COMPS_PER_SCAN = 4;
    for (Iterator iter = metadata.markerSequence.iterator();
        iter.hasNext(); ) {
      MarkerSegment seg = (MarkerSegment) iter.next();
      if (seg instanceof SOSMarkerSegment) {
        segments.add(seg);
      }
    }
    int[] retval = null;
    numScans = 0;
    if (!segments.isEmpty()) {
      numScans = segments.size();
      retval = new int[numScans * SCAN_SIZE];
      int index = 0;
      for (int i = 0; i < numScans; i++) {
        SOSMarkerSegment sos = (SOSMarkerSegment) segments.get(i);
        retval[index++] = sos.componentSpecs.length; // num comps
        for (int j = 0; j < MAX_COMPS_PER_SCAN; j++) {
          if (j < sos.componentSpecs.length) {
            int compSel = sos.componentSpecs[j].componentSelector;
            for (int k = 0; k < sof.componentSpecs.length; k++) {
              if (compSel == sof.componentSpecs[k].componentId) {
                retval[index++] = k;
                break; // out of for over sof comps
              }
            }
          } else {
            retval[index++] = 0;
          }
        }
        retval[index++] = sos.startSpectralSelection;
        retval[index++] = sos.endSpectralSelection;
        retval[index++] = sos.approxHigh;
        retval[index++] = sos.approxLow;
      }
    }
    return retval;
  }

  /**
   * Finds all DQT marker segments and returns all the q
   * tables as a single array of JPEGQTables.
   */
  private JPEGQTable[] collectQTablesFromMetadata
  (JPEGMetadata metadata) {
    ArrayList tables = new ArrayList();
    Iterator iter = metadata.markerSequence.iterator();
    while (iter.hasNext()) {
      MarkerSegment seg = (MarkerSegment) iter.next();
      if (seg instanceof DQTMarkerSegment) {
        DQTMarkerSegment dqt =
            (DQTMarkerSegment) seg;
        tables.addAll(dqt.tables);
      }
    }
    JPEGQTable[] retval = null;
    if (tables.size() != 0) {
      retval = new JPEGQTable[tables.size()];
      for (int i = 0; i < retval.length; i++) {
        retval[i] =
            new JPEGQTable(((DQTMarkerSegment.Qtable) tables.get(i)).data);
      }
    }
    return retval;
  }

  /**
   * Finds all DHT marker segments and returns all the q
   * tables as a single array of JPEGQTables.  The metadata
   * must not be for a progressive image, or an exception
   * will be thrown when two Huffman tables with the same
   * table id are encountered.
   */
  private JPEGHuffmanTable[] collectHTablesFromMetadata
  (JPEGMetadata metadata, boolean wantDC) throws IIOException {
    ArrayList tables = new ArrayList();
    Iterator iter = metadata.markerSequence.iterator();
    while (iter.hasNext()) {
      MarkerSegment seg = (MarkerSegment) iter.next();
      if (seg instanceof DHTMarkerSegment) {
        DHTMarkerSegment dht =
            (DHTMarkerSegment) seg;
        for (int i = 0; i < dht.tables.size(); i++) {
          DHTMarkerSegment.Htable htable =
              (DHTMarkerSegment.Htable) dht.tables.get(i);
          if (htable.tableClass == (wantDC ? 0 : 1)) {
            tables.add(htable);
          }
        }
      }
    }
    JPEGHuffmanTable[] retval = null;
    if (tables.size() != 0) {
      DHTMarkerSegment.Htable[] htables =
          new DHTMarkerSegment.Htable[tables.size()];
      tables.toArray(htables);
      retval = new JPEGHuffmanTable[tables.size()];
      for (int i = 0; i < retval.length; i++) {
        retval[i] = null;
        for (int j = 0; j < tables.size(); j++) {
          if (htables[j].tableID == i) {
            if (retval[i] != null) {
              throw new IIOException("Metadata has duplicate Htables!");
            }
            retval[i] = new JPEGHuffmanTable(htables[j].numCodes,
                htables[j].values);
          }
        }
      }
    }

    return retval;
  }

  /////////// End of metadata handling

  ////////////// ColorSpace conversion

  private int getSrcCSType(ImageTypeSpecifier type) {
    return getSrcCSType(type.getColorModel());
  }

  private int getSrcCSType(RenderedImage rimage) {
    return getSrcCSType(rimage.getColorModel());
  }

  private int getSrcCSType(ColorModel cm) {
    int retval = JPEG.JCS_UNKNOWN;
    if (cm != null) {
      boolean alpha = cm.hasAlpha();
      ColorSpace cs = cm.getColorSpace();
      switch (cs.getType()) {
        case ColorSpace.TYPE_GRAY:
          retval = JPEG.JCS_GRAYSCALE;
          break;
        case ColorSpace.TYPE_RGB:
          if (alpha) {
            retval = JPEG.JCS_RGBA;
          } else {
            retval = JPEG.JCS_RGB;
          }
          break;
        case ColorSpace.TYPE_YCbCr:
          if (alpha) {
            retval = JPEG.JCS_YCbCrA;
          } else {
            retval = JPEG.JCS_YCbCr;
          }
          break;
        case ColorSpace.TYPE_3CLR:
          if (cs == JPEG.JCS.getYCC()) {
            if (alpha) {
              retval = JPEG.JCS_YCCA;
            } else {
              retval = JPEG.JCS_YCC;
            }
          }
        case ColorSpace.TYPE_CMYK:
          retval = JPEG.JCS_CMYK;
          break;
      }
    }
    return retval;
  }

  private int getDestCSType(ImageTypeSpecifier destType) {
    ColorModel cm = destType.getColorModel();
    boolean alpha = cm.hasAlpha();
    ColorSpace cs = cm.getColorSpace();
    int retval = JPEG.JCS_UNKNOWN;
    switch (cs.getType()) {
      case ColorSpace.TYPE_GRAY:
        retval = JPEG.JCS_GRAYSCALE;
        break;
      case ColorSpace.TYPE_RGB:
        if (alpha) {
          retval = JPEG.JCS_RGBA;
        } else {
          retval = JPEG.JCS_RGB;
        }
        break;
      case ColorSpace.TYPE_YCbCr:
        if (alpha) {
          retval = JPEG.JCS_YCbCrA;
        } else {
          retval = JPEG.JCS_YCbCr;
        }
        break;
      case ColorSpace.TYPE_3CLR:
        if (cs == JPEG.JCS.getYCC()) {
          if (alpha) {
            retval = JPEG.JCS_YCCA;
          } else {
            retval = JPEG.JCS_YCC;
          }
        }
      case ColorSpace.TYPE_CMYK:
        retval = JPEG.JCS_CMYK;
        break;
    }
    return retval;
  }

  private int getDefaultDestCSType(ImageTypeSpecifier type) {
    return getDefaultDestCSType(type.getColorModel());
  }

  private int getDefaultDestCSType(RenderedImage rimage) {
    return getDefaultDestCSType(rimage.getColorModel());
  }

  private int getDefaultDestCSType(ColorModel cm) {
    int retval = JPEG.JCS_UNKNOWN;
    if (cm != null) {
      boolean alpha = cm.hasAlpha();
      ColorSpace cs = cm.getColorSpace();
      switch (cs.getType()) {
        case ColorSpace.TYPE_GRAY:
          retval = JPEG.JCS_GRAYSCALE;
          break;
        case ColorSpace.TYPE_RGB:
          if (alpha) {
            retval = JPEG.JCS_YCbCrA;
          } else {
            retval = JPEG.JCS_YCbCr;
          }
          break;
        case ColorSpace.TYPE_YCbCr:
          if (alpha) {
            retval = JPEG.JCS_YCbCrA;
          } else {
            retval = JPEG.JCS_YCbCr;
          }
          break;
        case ColorSpace.TYPE_3CLR:
          if (cs == JPEG.JCS.getYCC()) {
            if (alpha) {
              retval = JPEG.JCS_YCCA;
            } else {
              retval = JPEG.JCS_YCC;
            }
          }
        case ColorSpace.TYPE_CMYK:
          retval = JPEG.JCS_YCCK;
          break;
      }
    }
    return retval;
  }

  private boolean isSubsampled(SOFMarkerSegment.ComponentSpec[] specs) {
    int hsamp0 = specs[0].HsamplingFactor;
    int vsamp0 = specs[0].VsamplingFactor;
    for (int i = 1; i < specs.length; i++) {
      if ((specs[i].HsamplingFactor != hsamp0) ||
          (specs[i].HsamplingFactor != hsamp0)) {
        return true;
      }
    }
    return false;
  }

  ////////////// End of ColorSpace conversion

  ////////////// Native methods and callbacks

  /**
   * Sets up static native structures.
   */
  private static native void initWriterIDs(Class qTableClass,
      Class huffClass);

  /**
   * Sets up per-writer native structure and returns a pointer to it.
   */
  private native long initJPEGImageWriter();

  /**
   * Sets up native structures for output stream
   */
  private native void setDest(long structPointer);

  /**
   * Returns <code>true</code> if the write was aborted.
   */
  private native boolean writeImage(long structPointer,
      byte[] data,
      int inCsType, int outCsType,
      int numBands,
      int[] bandSizes,
      int srcWidth,
      int destWidth, int destHeight,
      int stepX, int stepY,
      JPEGQTable[] qtables,
      boolean writeDQT,
      JPEGHuffmanTable[] DCHuffmanTables,
      JPEGHuffmanTable[] ACHuffmanTables,
      boolean writeDHT,
      boolean optimizeHuffman,
      boolean progressive,
      int numScans,
      int[] scans,
      int[] componentIds,
      int[] HsamplingFactors,
      int[] VsamplingFactors,
      int[] QtableSelectors,
      boolean haveMetadata,
      int restartInterval);


  /**
   * Writes the metadata out when called by the native code,
   * which will have already written the header to the stream
   * and established the library state.  This is simpler than
   * breaking the write call in two.
   */
  private void writeMetadata() throws IOException {
    if (metadata == null) {
      if (writeDefaultJFIF) {
        JFIFMarkerSegment.writeDefaultJFIF(ios,
            thumbnails,
            iccProfile,
            this);
      }
      if (writeAdobe) {
        AdobeMarkerSegment.writeAdobeSegment(ios, newAdobeTransform);
      }
    } else {
      metadata.writeToStream(ios,
          ignoreJFIF,
          forceJFIF,
          thumbnails,
          iccProfile,
          ignoreAdobe,
          newAdobeTransform,
          this);
    }
  }

  /**
   * Write out a tables-only image to the stream.
   */
  private native void writeTables(long structPointer,
      JPEGQTable[] qtables,
      JPEGHuffmanTable[] DCHuffmanTables,
      JPEGHuffmanTable[] ACHuffmanTables);

  /**
   * Put the scanline y of the source ROI view Raster into the
   * 1-line Raster for writing.  This handles ROI and band
   * rearrangements, and expands indexed images.  Subsampling is
   * done in the native code.
   * This is called by the native code.
   */
  private void grabPixels(int y) {

    Raster sourceLine = null;
    if (indexed) {
      sourceLine = srcRas.createChild(sourceXOffset,
          sourceYOffset + y,
          sourceWidth, 1,
          0, 0,
          new int[]{0});
      // If the image has BITMASK transparency, we need to make sure
      // it gets converted to 32-bit ARGB, because the JPEG encoder
      // relies upon the full 8-bit alpha channel.
      boolean forceARGB =
          (indexCM.getTransparency() != Transparency.OPAQUE);
      BufferedImage temp = indexCM.convertToIntDiscrete(sourceLine,
          forceARGB);
      sourceLine = temp.getRaster();
    } else {
      sourceLine = srcRas.createChild(sourceXOffset,
          sourceYOffset + y,
          sourceWidth, 1,
          0, 0,
          srcBands);
    }
    if (convertTosRGB) {
      if (debug) {
        System.out.println("Converting to sRGB");
      }
      // The first time through, converted is null, so
      // a new raster is allocated.  It is then reused
      // on subsequent lines.
      converted = convertOp.filter(sourceLine, converted);
      sourceLine = converted;
    }
    if (isAlphaPremultiplied) {
      WritableRaster wr = sourceLine.createCompatibleWritableRaster();
      int[] data = null;
      data = sourceLine.getPixels(sourceLine.getMinX(), sourceLine.getMinY(),
          sourceLine.getWidth(), sourceLine.getHeight(),
          data);
      wr.setPixels(sourceLine.getMinX(), sourceLine.getMinY(),
          sourceLine.getWidth(), sourceLine.getHeight(),
          data);
      srcCM.coerceData(wr, false);
      sourceLine = wr.createChild(wr.getMinX(), wr.getMinY(),
          wr.getWidth(), wr.getHeight(),
          0, 0,
          srcBands);
    }
    raster.setRect(sourceLine);
    if ((y > 7) && (y % 8 == 0)) {  // Every 8 scanlines
      cbLock.lock();
      try {
        processImageProgress((float) y / (float) sourceHeight * 100.0F);
      } finally {
        cbLock.unlock();
      }
    }
  }

  /**
   * Aborts the current write in the native code
   */
  private native void abortWrite(long structPointer);

  /**
   * Resets native structures
   */
  private native void resetWriter(long structPointer);

  /**
   * Releases native structures
   */
  private static native void disposeWriter(long structPointer);

  private static class JPEGWriterDisposerRecord implements DisposerRecord {

    private long pData;

    public JPEGWriterDisposerRecord(long pData) {
      this.pData = pData;
    }

    public synchronized void dispose() {
      if (pData != 0) {
        disposeWriter(pData);
        pData = 0;
      }
    }
  }

  /**
   * This method is called from native code in order to write encoder
   * output to the destination.
   *
   * We block any attempt to change the writer state during this
   * method, in order to prevent a corruption of the native encoder
   * state.
   */
  private void writeOutputData(byte[] data, int offset, int len)
      throws IOException {
    cbLock.lock();
    try {
      ios.write(data, offset, len);
    } finally {
      cbLock.unlock();
    }
  }

  private Thread theThread = null;
  private int theLockCount = 0;

  private synchronized void setThreadLock() {
    Thread currThread = Thread.currentThread();
    if (theThread != null) {
      if (theThread != currThread) {
        // it looks like that this reader instance is used
        // by multiple threads.
        throw new IllegalStateException("Attempt to use instance of " +
            this + " locked on thread " +
            theThread + " from thread " +
            currThread);
      } else {
        theLockCount++;
      }
    } else {
      theThread = currThread;
      theLockCount = 1;
    }
  }

  private synchronized void clearThreadLock() {
    Thread currThread = Thread.currentThread();
    if (theThread == null || theThread != currThread) {
      throw new IllegalStateException("Attempt to clear thread lock form wrong thread. " +
          "Locked thread: " + theThread +
          "; current thread: " + currThread);
    }
    theLockCount--;
    if (theLockCount == 0) {
      theThread = null;
    }
  }

  private CallBackLock cbLock = new CallBackLock();

  private static class CallBackLock {

    private State lockState;

    CallBackLock() {
      lockState = State.Unlocked;
    }

    void check() {
      if (lockState != State.Unlocked) {
        throw new IllegalStateException("Access to the writer is not allowed");
      }
    }

    private void lock() {
      lockState = State.Locked;
    }

    private void unlock() {
      lockState = State.Unlocked;
    }

    private static enum State {
      Unlocked,
      Locked
    }
  }
}
